package com.sc.core.util;

import com.sc.core.function.Function;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.experimental.Accessors;
import org.springframework.util.StringUtils;

import java.lang.reflect.*;
import java.util.*;
import java.util.function.BiFunction;

@Accessors(chain = true)
public class TreeHandler<O, T>{
    private final List<O> origin;
    private Class<T> target;
    private List<Method> targetMethods;
    @Setter
    private Function<O, ?> originPrimaryMethod;
    @Setter
    private Function<T, ?> targetPrimaryMethod;
    @Setter
    private Function<O, ?> originParentMethod;
    @Setter
    private Function<T, ?> targetParentMethod;
    @Setter
    private Function<T, List<T>> childrenMethod;

    public class Tree {
        List<T> treeData;

        Tree(List<T> data) {
            treeData = data;
        }

        public List<T> toList(){
            return treeData;
        }

        @SuppressWarnings("unchecked")
        public <LT> Tree recordLinked(Function<T, LT> getLinkedMethod){
            List<LT> initLinked = new ArrayList<>();
            iteration(treeData, (data, linked) -> {
                List<LT> currentLinked = (List<LT>)getLinkedMethod.apply(data);
                if (currentLinked == null) {
                    currentLinked = new ArrayList<>();
                }
                currentLinked.addAll(0, (List<LT>)linked);
                currentLinked.add((LT)targetPrimaryMethod.apply(data));
                return currentLinked;
            }, initLinked);
            return this;
        }

        public Tree each(Function<T, ?> f) {
            iteration(treeData, f);
            return this;
        }

        public Tree each(BiFunction<T, Object, ?> f, Object param) {
            iteration(treeData, f, param);
            return this;
        }

        @Override
        public String toString() {
            return treeData.toString();
        }
    }


    TreeHandler(List<O> origin) {
        this.origin = origin;
    }

    public static <O, T> TreeHandler<O, T> of(List<O> origin) {
        return new TreeHandler<>(origin);
    }

    @SuppressWarnings("unchecked")
    public static <O, T> TreeHandler<O, T>.Tree ofTree(List<T> origin) {
        TreeHandler<O, T> treeHandler = new TreeHandler<>(null);
        treeHandler.target = (Class<T>) origin.get(0).getClass();
        treeHandler.getDataMethodInit((Class<O>) treeHandler.target);

        return treeHandler.new Tree(origin);
    }

    @SneakyThrows
    @SuppressWarnings("unchecked")
    public Tree getTree(Class<T> target) {
        if (origin.isEmpty()) {
            return new Tree(new ArrayList<>());
        }
        this.target = target;
        Class<O> oClass   = (Class<O>) origin.get(0).getClass();
        getDataMethodInit(oClass);

        targetMethods     = Arrays.stream(target.getMethods()).toList();

        Constructor<T> constructor = gettConstructor(target);
        Map<Object, T> map = new HashMap<>();

        origin.forEach(o -> {
            T t = target.isRecord()
                    ? getTargetRecord(o, constructor, oClass)
                    : getTargetClass(o, constructor, oClass);

            map.put(targetPrimaryMethod.apply(t), t);
        });

        return new Tree(getTree(map));
    }

    @SneakyThrows
    @SuppressWarnings("unchecked")
    private void getDataMethodInit(Class<O> originClass){
        if (originParentMethod == null) {
            originParentMethod = makeMethodFunction(originClass, originClass.isRecord() ? "pid" : "getPid");
        }
        if (targetParentMethod == null) {
            targetParentMethod = makeMethodFunction(target, target.isRecord() ? "pid" : "getPid");
        }
        if (originPrimaryMethod == null) {
            originPrimaryMethod = makeMethodFunction(originClass, originClass.isRecord() ? "id" : "getId");
        }
        if (targetPrimaryMethod == null) {
            targetPrimaryMethod = makeMethodFunction(target, target.isRecord() ? "id" : "getId");
        }
        if (childrenMethod == null) {
            childrenMethod = (Function<T, List<T>>) makeMethodFunction(target, target.isRecord() ? "children" : "getChildren");
        }
    }

    private <MT> Function<MT, ?> makeMethodFunction(Class<MT> mtClass, String methodName) {
        return new Function<>() {
            @SneakyThrows
            @Override
            public Object apply(MT mt) {
                return mtClass.getMethod(methodName).invoke(mt);
            }

            @Override
            public String getMethodName() {
                return methodName;
            }

            @Override
            @SneakyThrows
            public String getFieldName() {
                boolean isRecord = mtClass.isRecord();
                if (isRecord) {
                    return methodName;
                }
                return methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
            }

            @Override
            public String getClassName(){
                return mtClass.getName();
            }
        };
    }

    private void iteration(List<T> tree, BiFunction<T, Object, ?> callable, Object param){
        if (callable == null) return;

        tree.forEach(t -> {
            Object par = callable.apply(t, param);
            if (!childrenMethod.apply(t).isEmpty()){
                iteration(childrenMethod.apply(t), callable, par);
            }
        });
    }

    private void iteration(List<T> tree, Function<T, ?> callable){
        if (callable == null) return;

        tree.forEach(t -> {
            callable.apply(t);
            if (!childrenMethod.apply(t).isEmpty()){
                iteration(childrenMethod.apply(t), callable);
            }
        });
    }

    @SneakyThrows
    private ArrayList<T> getTree(Map<Object, T> map) {
        ArrayList<T> res = new ArrayList<>();
        map.forEach((k, v) -> {
            Object parentValue = targetParentMethod.apply(v);
            if (parentValue.equals(0) || parentValue.equals("0") || parentValue.equals(0L) || parentValue.equals("")) {
                res.add(v);
            } else {
                childrenMethod.apply(map.get(parentValue)).add(v);
            }
        });
        return res;
    }

    @SneakyThrows
    private T getTargetClass(O o, Constructor<T> constructor, Class<?> oClass) {
        T t = constructor.newInstance();
        List<String> oMethods = Arrays.stream(oClass.getMethods()).map(Method::getName).toList();

        for (Method method : targetMethods) {
            if (method.getName().startsWith("set")) {
                Object value = null;
                String fieldName = StringUtils.uncapitalize(method.getName().substring(3));
                if (fieldName.equals(targetPrimaryMethod.getFieldName())) {
                    value = originPrimaryMethod.apply(o);
                } else if (fieldName.equals(targetParentMethod.getFieldName())) {
                    value = originParentMethod.apply(o);
                } else if (fieldName.equals(childrenMethod.getFieldName()) || method.getParameters()[0].getType().getName().equals("java.util.List")){
                    value = new ArrayList<T>();
                } else if(oMethods.contains("get" + StringUtils.capitalize(fieldName))){
                    value = oClass.getMethod("get" + StringUtils.capitalize(fieldName)).invoke(o);
                }

                method.invoke(t, value);
            }
        }
        return t;
    }

    @SneakyThrows
    private T getTargetRecord(O o, Constructor<T> constructor, Class<?> oClass) {
        Parameter[] parameters = constructor.getParameters();
        List<String> oMethods  = Arrays.stream(oClass.getMethods()).map(Method::getName).toList();
        Object[] objects = new Object[parameters.length];
        for (int i = 0; i < parameters.length; i++) {
            objects[i] = null;
            if (parameters[i].getName().equals(targetPrimaryMethod.getFieldName())) {
                objects[i] = originPrimaryMethod.apply(o);
            }else if (parameters[i].getName().equals(targetParentMethod.getFieldName())) {
                objects[i] = originParentMethod.apply(o);
            }else if (parameters[i].getName().equals(childrenMethod.getFieldName()) || parameters[i].getType().getName().equals("java.util.List")){
                objects[i] = new ArrayList<T>();
            }else if (oMethods.contains("get" + StringUtils.capitalize(parameters[i].getName()))){
                objects[i] = oClass.getMethod("get" + StringUtils.capitalize(parameters[i].getName())).invoke(o);
            }
        }

        return constructor.newInstance(objects);
    }

    @SuppressWarnings("unchecked")
    private Constructor<T> gettConstructor(Class<T> target) {
        Constructor<T> constructor;
        constructor = (Constructor<T>) target.getConstructors()[0];
        return constructor;
    }
}
